package ch.jamme.util;

import static ch.jamme.util.IntrospectionUtil.*;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;

import ch.jamme.exception.*;

/**
 * A static class the provides some utility methods to introspect the objects to be marshalled
 */
public class IntrospectionUtil {
	
	private static boolean isDebugEnabled = Logger.isDebugEnabledFor(IntrospectionUtil.class);

	private IntrospectionUtil() { }
	
	private static Map<String, Class<?>> classByNameCache = new HashMap<String, Class<?>>();

	/**
	 * Instanciate an Object starting from its Class using the defualt no-args constructor
	 * @param clazz The Class of the object to be instanciated
	 * @return An object that's an instance of the given class
	 */
	public static Object getObjectByEmptyConstructor(Class<?> clazz) {
		try {
			return clazz.newInstance();
		} catch (Exception e) {
			throw new JammeException("Error invoking empty constructor on class: " + clazz.getName());
		}
	}
	
	/**
	 * Instanciate an Object starting from its Class using the constructor having a String as parameter
	 * @param clazz The Class of the object to be instanciated
	 * @param value The String passed to the Object contructor
	 * @return An object that's an instance of the given class and constructed using the given value
	 */
	public static Object getObjectByStringConstructor(Class<?> clazz, String value) {
		if (value == null) return getObjectByEmptyConstructor(clazz);
		
		if (clazz.isEnum()) {
			for (Object enumItem : clazz.getEnumConstants()) if (enumItem.toString().equals(value)) return enumItem;
			return null;
		}
		
		if (clazz.isPrimitive()) {
			value = value.trim();
			if (clazz == Integer.TYPE) return value.length() == 0 ? 0 : Integer.valueOf(value);
			if (clazz == Long.TYPE) return value.length() == 0 ? 0L : Long.valueOf(value);
			if (clazz == Double.TYPE) return value.length() == 0 ? 0.0 : Double.valueOf(value);
			if (clazz == Float.TYPE) return value.length() == 0 ? 0.0 : Float.valueOf(value);
			if (clazz == Boolean.TYPE) return value.length() == 0 ? false : Boolean.valueOf(value);
		}
		
		if (Boolean.class.isAssignableFrom(clazz)) return value.trim().equalsIgnoreCase("true") ? Boolean.TRUE : Boolean.FALSE;
		
		if (Number.class.isAssignableFrom(clazz)) {
			value = value.trim();
			if (value.length() == 0) value = "0";
		}
		
		try {
			return clazz.getConstructor(String.class).newInstance(value);
		} catch (Exception e) {
			throw new JammeException("Error invoking String constructor on : " + clazz.getName() + " with value " + value, e);
		}
	}
	
	/**
	 * Get the Class starting from its name
	 * @param className The name of the class to be found
	 * @return The Class having the given class name
	 */
	public static Class<?> getClassByName(String className) {
		Class<?> clazz = classByNameCache.get(className);
		if (clazz == null) {
			try {
				clazz = Class.forName(className);
				classByNameCache.put(className, clazz);
			} catch (ClassNotFoundException e) {
				throw new JammeException("Unknown class: " + className);
			}
		}
		return clazz;
	}

	// ////////////////////////////////////////////////////////////////////////
	// /// Generic types
	// ////////////////////////////////////////////////////////////////////////

	public static Type[] getGenericTypes(Field field) {
		if (field == null) return null;
		return getGenericTypes(field.getGenericType());
	}
	
	public static Type[] getGenericTypes(Type type) {
		if (type == null || !(type instanceof ParameterizedType)) return null;
		return ((ParameterizedType)type).getActualTypeArguments();
	}
	
	public static Class<?> asClass(Type type) {
		if (type instanceof ParameterizedType) return (Class<?>)((ParameterizedType)type).getRawType();
		return type instanceof Class ? (Class<?>)type : null;
	}
	
	// ////////////////////////////////////////////////////////////////////////
	// /// Fields
	// ////////////////////////////////////////////////////////////////////////

	private static Map<Class<?>, Map<String, Field>> fieldsCache = new HashMap<Class<?>, Map<String, Field>>();

	/**
	 * Returns all the fields of the given class (and all its superclass)
	 * @param clazz The Class to be introspected
	 * @return The list of all the fields of the given class (and all its superclass)
	 */
	public static Collection<Field> getAllFields(Class<?> clazz) {
		return getFieldMap(clazz).values();
	}
	
	/**
	 * Returns all the fields of the given class (and all its superclass) indexed by the correspondant field name
	 * @param clazz The Class to be introspected
	 * @return The Map having as values all the fields of the given class (and all its superclass) and as key the the correspondant names
	 */
	public static Map<String, Field> getFieldMap(Class<?> clazz) {
		Map<String, Field> fieldMap = fieldsCache.get(clazz);
		if (fieldMap == null) fieldMap = indexFields(clazz);
		return fieldMap;
	}
	
	private static Map<String, Field> indexFields(Class<?> clazz) {
		Map<String, Field> fieldMap = new TreeMap<String, Field>();
		if (clazz == null) return fieldMap;
		for (Field field : clazz.getDeclaredFields()) fieldMap.put(field.getName(), field);
		fieldMap.putAll(getFieldMap(clazz.getSuperclass()));
		fieldsCache.put(clazz, fieldMap);
		return fieldMap;
	}

	/**
	 * Make a field accessible
	 * @param field The field to be made accessible
	 * @return True if the field can be made accessible and it makes sense to serialize it ù
	 * 		   (i.e. it is neither static, nor final, nor static) 
	 */
	public static boolean makeAccesible(Field field) {
		int mod = field.getModifiers();
		// If the field is declared as final, static or transient doesn't make it accessible
		if (Modifier.isFinal(mod) || Modifier.isStatic(mod) || Modifier.isTransient(mod)) return false;
		field.setAccessible(true);
		return true;
	}

	// ////////////////////////////////////////////////////////////////////////
	// /// Accessors
	// ////////////////////////////////////////////////////////////////////////
	
	/**
	 * Gets the value of a field belonging to the given declaring class from a given object.
	 * If the field donesn't actually belong to the object class it tries to get the value using the corresponding read method.
	 * @param declaringClass The class that declares the field to be introspected
	 * @param field The field to be read
	 * @param object The object from where get the value
	 * @return The field's value on the given object
	 */
	public static Object getFieldValue(Class<?> declaringClass, Field field, Object object) throws Exception {
		Object value = null;
		if (declaringClass.isAssignableFrom(object.getClass())) {
			if (isDebugEnabled) Logger.debug(IntrospectionUtil.class, "Getting field " + field + " value from " + object);
			value = field.get(object);
		} else {
			if (isDebugEnabled) Logger.debug(IntrospectionUtil.class, "Field " + field + " doesn't belong to " + object + "; try to get using the read method");
			value = new PropertyDescriptor(field.getName(), getCommonClass(declaringClass, object)).getReadMethod().invoke(object);
		}
		if (isDebugEnabled) Logger.debug(IntrospectionUtil.class, "Field " + field + " in " + object + " has value " + value);
		return value;
	}
	
	/**
	 * Sets the value of a field belonging to the given declaring class from a given object.
	 * If the field donesn't actually belong to the object class it tries to set the value using the corresponding write method.
	 * @param declaringClass The class that declares the field to be introspected
	 * @param field The field to be written
	 * @param object The object into which set the value
	 * @param value The value to be setted into the field of the given object
	 */
	public static void setFieldValue(Class<?> declaringClass, Field field, Object object, Object value) throws Exception {
		if (declaringClass.isAssignableFrom(object.getClass())) {
			if (isDebugEnabled) Logger.debug(IntrospectionUtil.class, "Setting field " + field + " value to " + object);
			field.set(object, value);
		} else {
			if (isDebugEnabled) Logger.debug(IntrospectionUtil.class, "Field " + field + " doesn't belong to " + object + "; try to set using the write method");
			new PropertyDescriptor(field.getName(), getCommonClass(declaringClass, object)).getWriteMethod().invoke(object, value);
		}
	}
	
	private static Class<?> getCommonClass(Class<?> clazz, Object object) {
		if (clazz == null) return null;
		if (clazz.isInstance(object)) return clazz;
		for (Class<?> interfaze : clazz.getInterfaces()) {
			Class<?> result = getCommonClass(interfaze, object);
			if (result != null) return result;
		}
		return getCommonClass(clazz.getSuperclass(), object);
	}
}
